#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/select.h>

#include "imbus_internal.h"
#include "im_main_loop_unix.h"
#include "im_io_unix.h"
#include "im_hash.h"
#include "im_array.h"

#define MAIN_LOOP_VERBOSE 1

/* =============================== Internal data ========================== */
#define IM_IO_WATCH_ID_MAX      FD_SETSIZE

typedef struct __IMAnyWatchNode
{
    IMWatchType     type;
    IMInt           id;
    IMWatchFunc     func;
    IMPointer       data;
    IMDestroyFunc   data_destroy_func;
} IMAnyWatchNode;

typedef struct __IMIOWatchNode
{
    IMWatchType     type;
    IMInt           id;
    IMWatchFunc     func;
    IMPointer       data;
    IMDestroyFunc   data_destroy_func;
    IMInt           fd;
    IMIOChannel    *channel;
} IMIOWatchNode;

typedef struct __IMTimeoutWatchNode
{
    IMWatchType     type;
    IMInt           id;
    IMWatchFunc     func;
    IMPointer       data;
    IMDestroyFunc   data_destroy_func;
    IMInt           interval;
    IMUInt64        next_time;
} IMTimeoutWatchNode;

typedef union __IMWatchNode
{
    IMWatchType         type;
    IMAnyWatchNode      any;
    IMIOWatchNode       io;
    IMTimeoutWatchNode  timeout;
} IMWatchNode;

struct _IMMainLoopUnix
{
    IMMainLoop           parent_object;
    IMHashTable         *nodes;

    IMUInt               serial;
    IMInt                depth;
};

struct _IMMainLoopUnixClass
{
    IMMainLoopClass parent_class;
};

/* ============================= Internal functions ======================= */
static void
__destroy_node_handler (IMPointer node)
{
    IMWatchNode *p = (IMWatchNode*)node;

    if (p->type == IM_WATCH_IO_READ || p->type == IM_WATCH_IO_WRITE)
        im_object_unref (p->io.channel);
    if (p->any.data_destroy_func)
        p->any.data_destroy_func (p->any.data);

    im_slice_delete (IMWatchNode, p);
}

static void
__add_node (IMMainLoopUnix *loop, IMWatchNode *node)
{
    IMSize idx;
    im_hash_table_insert (loop->nodes, IM_INT_TO_POINTER(node->any.id), node);
    loop->serial++;
    if (loop->serial == 0)
        loop->serial = 1;
}

static IMBool
__remove_node (IMMainLoopUnix *loop, IMInt id)
{
    IMWatchNode *node;
    IMSize i;
    node = im_hash_table_lookup (loop->nodes, IM_INT_TO_POINTER(id));

    if (node) {
        im_hash_table_remove (loop->nodes, IM_INT_TO_POINTER(id));
        loop->serial++;
        if (loop->serial == 0)
            loop->serial = 1;
        return TRUE;
    }
    return FALSE;
}

/* Return TRUE if any callback function is called during this iteration. */
static IMBool
__do_iteration (IMMainLoopUnix *loop, IMBool may_block)
{
    IMInt timeout = -1;
    IMInt max_fd = 0;
    IMInt read_fdnum = 0;
    IMInt write_fdnum = 0;
    fd_set read_fds;
    fd_set write_fds;
    IMInt orig_serial;
    IMInt orig_depth;
    IMInt result;
    IMBool ret = FALSE;
    IMBool call;
    IMUInt64 now;
    struct timeval wait_tv;
    IMWatchNode *node;
    IMHashIterator *iter;

#if MAIN_LOOP_VERBOSE
    _im_verbose ("Iteration block=%d depth=%d\n", may_block, loop->depth);
#endif

    /* No watch available, just sleep for a while and return.
     * Actually, it shouldn't happen. */
    if (im_hash_table_size (loop->nodes) == 0) {
        usleep (500000);
        return FALSE;
    }

    /* Record some states */
    orig_depth = loop->depth;
    orig_serial = loop->serial;

    if (!may_block) timeout = 0;

    iter = im_hash_table_get_iterator (loop->nodes);
    now = im_get_current_time_in_milliseconds ();

    /* Gather fds of all io watches. */
    /* Calculate the minimal timeout interval */
    FD_ZERO (&read_fds);
    FD_ZERO (&write_fds);
    while (im_hash_iterator_valid (iter)) {
        node = (IMWatchNode*) im_hash_iterator_get_value (iter);
        if (node->io.type == IM_WATCH_IO_READ) {
            FD_SET (node->io.fd, &read_fds);
            if (max_fd < node->io.fd) max_fd = node->io.fd;
            ++read_fdnum;
        } else if (node->io.type == IM_WATCH_IO_WRITE) {
            FD_SET (node->io.fd, &write_fds);
            if (max_fd < node->io.fd) max_fd = node->io.fd;
            ++write_fdnum;
        } else if (node->io.type == IM_WATCH_TIMEOUT) {
            if (node->timeout.next_time <= now)
                timeout = 0;
            else if (timeout == -1 || (node->timeout.next_time - now) < timeout)
                timeout = node->timeout.next_time - now;
        }
        if (!im_hash_iterator_next (iter))
            break;
    }
    im_hash_iterator_destroy (iter);

    if (timeout >= 0) {
        wait_tv.tv_sec = timeout / 1000;
        wait_tv.tv_usec = (timeout % 1000) * 1000;
    }

    /* Call select to select the fds and wait for a certain timeout. */
#if MAIN_LOOP_VERBOSE
    _im_verbose ("Do select: now=%lld, max_fd=%d, timeout=%d\n",
                 now, max_fd, timeout);
#endif

    result = select (max_fd + 1,
                     (read_fdnum ? &read_fds : 0),
                     (write_fdnum ? &write_fds : 0),
                     NULL,
                     (timeout >= 0 ? &wait_tv : 0));
    if (result < 0)
        return FALSE;

    /* Check if there are some callbacks need calling.*/
    now = im_get_current_time_in_milliseconds ();
    iter = im_hash_table_get_iterator (loop->nodes);
    while (im_hash_iterator_valid (iter)) {
        call = FALSE;
        node = (IMWatchNode*) im_hash_iterator_get_value (iter);
        if ((node->io.type == IM_WATCH_IO_READ && FD_ISSET(node->io.fd, &read_fds)) ||
            (node->io.type == IM_WATCH_IO_WRITE && FD_ISSET(node->io.fd, &write_fds))) {
            call = TRUE;
        } else if (node->io.type == IM_WATCH_TIMEOUT && now >= node->timeout.next_time) {
            call = TRUE;
            node->timeout.next_time += (IMUInt64)(node->timeout.interval);
        }

        if (call) {
            ret = TRUE;
            if (!node->any.func ((IMMainLoop*)loop, node->any.id, node->any.data))
                __remove_node (loop, node->any.id);
        }

        if (orig_depth != loop->depth || orig_serial != loop->serial)
            break;
        if (!im_hash_iterator_next (iter))
            break;
    }
    im_hash_iterator_destroy (iter);

    return ret;
};

static IMInt
__im_main_loop_unix_add_io_watch_internal (IMMainLoop      *loop,
                                           IMIOChannel     *channel,
                                           IMWatchType      type,
                                           IMWatchFunc      func,
                                           IMPointer        data,
                                           IMDestroyFunc    data_destroy_func)
{
    IMMainLoopUnix  *p = IM_MAIN_LOOP_UNIX(loop);
    IMIOChannelUnix *c = IM_IO_CHANNEL_UNIX(channel);
    IMWatchNode *node;
    IMInt fd;

    _im_return_val_if_fail (p != 0, -1);
    _im_return_val_if_fail (c != 0, -1);
    _im_return_val_if_fail (im_io_channel_valid (channel), -1);
    _im_return_val_if_fail (func != 0, -1);

    node = im_slice_new (IMWatchNode);
    _im_assert (node);

    fd = im_io_channel_unix_get_fd (channel);
    node->type      = type;
    node->io.id     = p->serial;
    node->io.func   = func;
    node->io.data   = data;
    node->io.data_destroy_func = data_destroy_func;
    node->io.fd     = fd;
    node->io.channel= im_object_ref (c);

    __add_node (p, node);
    return node->io.id;
}

/* ============================== Class methods ========================== */
static IMInt
__im_main_loop_unix_add_io_read_watch (IMMainLoop      *loop,
                                       IMIOChannel     *channel,
                                       IMWatchFunc      func,
                                       IMPointer        data,
                                       IMDestroyFunc    data_destroy_func)
{
    return __im_main_loop_unix_add_io_watch_internal (loop,
                                                      channel,
                                                      IM_WATCH_IO_READ,
                                                      func,
                                                      data,
                                                      data_destroy_func);
}

static IMInt
__im_main_loop_unix_add_io_write_watch (IMMainLoop      *loop,
                                        IMIOChannel     *channel,
                                        IMWatchFunc      func,
                                        IMPointer        data,
                                        IMDestroyFunc    data_destroy_func)
{
    return __im_main_loop_unix_add_io_watch_internal (loop,
                                                      channel,
                                                      IM_WATCH_IO_WRITE,
                                                      func,
                                                      data,
                                                      data_destroy_func);
}

static IMInt
__im_main_loop_unix_add_timeout_watch (IMMainLoop       *loop,
                                       IMInt             interval,
                                       IMWatchFunc       func,
                                       IMPointer         data,
                                       IMDestroyFunc     data_destroy_func)
{
    IMMainLoopUnix  *p = IM_MAIN_LOOP_UNIX(loop);
    IMWatchNode *node;

    _im_return_val_if_fail (p != 0, -1);
    _im_return_val_if_fail (interval > 0, -1);
    _im_return_val_if_fail (func != 0, -1);

    node = im_slice_new (IMWatchNode);
    _im_assert (node);

    node->type              = IM_WATCH_TIMEOUT;
    node->timeout.id        = p->serial;
    node->timeout.func      = func;
    node->timeout.data      = data;
    node->timeout.data_destroy_func = data_destroy_func;
    node->timeout.interval  = interval;
    node->timeout.next_time = im_get_current_time_in_milliseconds ();
    node->timeout.next_time += interval;

    __add_node (p, node);
    return node->timeout.id;
}

IMWatchType
__im_main_loop_unix_get_watch_type (IMMainLoop *loop, IMInt watch_id)
{
    IMMainLoopUnix *p = IM_MAIN_LOOP_UNIX(loop);
    IMWatchNode *node;
    _im_return_val_if_fail (p != 0, IM_WATCH_NONE);

    node = im_hash_table_lookup (p->nodes, IM_INT_TO_POINTER(watch_id));

    if (node)
        return node->type;
    return IM_WATCH_NONE;
}

IMIOChannel*
__im_main_loop_unix_get_watch_io_channel (IMMainLoop *loop, IMInt watch_id)
{
    IMMainLoopUnix *p = IM_MAIN_LOOP_UNIX(loop);
    IMWatchNode *node;
    _im_return_val_if_fail (p != 0, IM_WATCH_NONE);

    node = im_hash_table_lookup (p->nodes, IM_INT_TO_POINTER(watch_id));

    if (node &&
        (node->type == IM_WATCH_IO_READ ||
         node->type == IM_WATCH_IO_WRITE))
        return node->io.channel;
    return 0;
}

IMInt
__im_main_loop_unix_get_watch_timeout (IMMainLoop *loop, IMInt watch_id)
{
    IMMainLoopUnix *p = IM_MAIN_LOOP_UNIX(loop);
    IMWatchNode *node;
    _im_return_val_if_fail (p != 0, IM_WATCH_NONE);

    node = im_hash_table_lookup (p->nodes, IM_INT_TO_POINTER(watch_id));

    if (node && node->type == IM_WATCH_TIMEOUT)
        return node->timeout.interval;
    return -1;
}

static IMBool
__im_main_loop_unix_remove_watch (IMMainLoop         *loop,
                                  IMInt               watch_id)
{
    IMMainLoopUnix  *p = IM_MAIN_LOOP_UNIX(loop);
    _im_return_val_if_fail (p != 0, -1);
    return __remove_node (p, watch_id);
}

static void
__im_main_loop_unix_run (IMMainLoop *loop)
{
    IMMainLoopUnix  *p = IM_MAIN_LOOP_UNIX(loop);
    IMInt exit_depth;
    _im_return_if_fail (p != 0);
    _im_assert (p->depth >= 0);

    im_object_ref (p);
    exit_depth = p->depth;
    p->depth ++;

    _im_verbose ("Running main loop %p: depth %d -> %d\n",
                 p, exit_depth, p->depth);

    while (p->depth != exit_depth)
        __do_iteration (p, TRUE);

    im_object_unref (p);
}

static IMBool
__im_main_loop_unix_iteration (IMMainLoop *loop, IMBool may_block)
{
    IMBool ret;
    IMMainLoopUnix  *p = IM_MAIN_LOOP_UNIX(loop);
    _im_return_val_if_fail (p != 0, FALSE);
    _im_assert (p->depth >= 0);

    /* Keep the main loop object with a reference. */
    im_object_ref (p);
    _im_verbose ("Do an iteration of main loop %p: depth=%d may_block=%d\n",
                 p, p->depth, may_block);
    ret = __do_iteration (p, may_block);
    im_object_unref (p);
    return ret;
}

static void
__im_main_loop_unix_quit (IMMainLoop *loop)
{
    IMMainLoopUnix  *p = IM_MAIN_LOOP_UNIX(loop);
    _im_return_if_fail (p != 0);
    _im_assert (p->depth > 0);
    p->depth --;

    _im_verbose ("Quit main loop %p: depth %d -> %d\n",
                 p, p->depth + 1, p->depth);
}

static IMBool
__im_main_loop_unix_is_running (IMMainLoop *loop)
{
    IMMainLoopUnix  *p = IM_MAIN_LOOP_UNIX(loop);
    _im_return_val_if_fail (p != 0, FALSE);
    _im_assert (p->depth >= 0);
    return p->depth > 0;
}

static void
__im_main_loop_unix_class_init (IMPointer klass)
{
    IMMainLoopClass *mcp = (IMMainLoopClass *)klass;

    mcp->add_io_read_watch  = __im_main_loop_unix_add_io_read_watch;
    mcp->add_io_write_watch = __im_main_loop_unix_add_io_write_watch;
    mcp->add_timeout_watch  = __im_main_loop_unix_add_timeout_watch;
    mcp->get_watch_type     = __im_main_loop_unix_get_watch_type;
    mcp->get_watch_io_channel = __im_main_loop_unix_get_watch_io_channel;
    mcp->get_watch_timeout  = __im_main_loop_unix_get_watch_timeout;
    mcp->remove_watch       = __im_main_loop_unix_remove_watch;
    mcp->run                = __im_main_loop_unix_run;
    mcp->iteration          = __im_main_loop_unix_iteration;
    mcp->quit               = __im_main_loop_unix_quit;
    mcp->is_running         = __im_main_loop_unix_is_running;
}

static void
__im_main_loop_unix_initialize (IMPointer obj)
{
    IMMainLoopUnix *p = IM_MAIN_LOOP_UNIX(obj);
    _im_return_if_fail (p != 0);

    p->nodes = im_hash_table_new_full (im_direct_hash,
                                       im_direct_equal,
                                       0,
                                       __destroy_node_handler);
    p->serial = 1;
    p->depth = 0;
}

static void
__im_main_loop_unix_finalize (IMPointer obj)
{
    IMMainLoopUnix *p = IM_MAIN_LOOP_UNIX(obj);
    _im_return_if_fail (p != 0);
    _im_assert (p->depth == 0);

    im_hash_table_destroy (p->nodes);
}

void
__im_type_register_main_loop_unix ()
{
    static IMTypeInfo im_main_loop_unix_type_info =
    {
        sizeof (IMMainLoopUnixClass),   /**< class_size >**/
        __im_main_loop_unix_class_init, /**< class_init >**/
        0,                              /**< class_finalize >**/

        sizeof (IMMainLoopUnix),        /**< instance_size >**/
        __im_main_loop_unix_initialize, /**< instance_init >**/
        __im_main_loop_unix_finalize,   /**< instance_finalize >**/
    };

    __im_type_register_builtin_class (IM_TYPE_MAIN_LOOP,
                                      IM_TYPE_MAIN_LOOP_UNIX,
                                      "MainLoopUnix",
                                      &im_main_loop_unix_type_info,
                                      TRUE);
}

IMMainLoop*
im_main_loop_unix_new ()
{
    return im_main_loop_new (IM_TYPE_MAIN_LOOP_UNIX);
}

/*
vi:ts=4:nowrap:ai:expandtab
*/
